package joe;

import java.util.ArrayList;
import java.util.Arrays;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFWGamepadState;
import static org.lwjgl.glfw.GLFW.*;

public class InputGlfw implements Input {
	private static final int JOYSTICK_MAX_COUNT = 4;

	private long window;
	private final byte[] keyState = new byte[(GLFW_KEY_LAST + 8) / 8];
	private int mouseState, mouseX, mouseY;
	private final boolean[] gamepadConnects = new boolean[JOYSTICK_MAX_COUNT];
	private final @NotNull GLFWGamepadState @NotNull [] gamepadStates = new GLFWGamepadState[JOYSTICK_MAX_COUNT];
	private final @NotNull ArrayList<@NotNull EventHandler> eventListeners = new ArrayList<>();
	private final @NotNull ArrayList<@NotNull EventHandler> eventHandlers = new ArrayList<>();

	@Override
	public void init() {
		if (window != 0)
			throw new IllegalStateException("already inited");
		try {
			window = ((RenderGles3)Engine.current().getRender()).getWindow();
			if (window == 0)
				throw new IllegalStateException("render is not inited");
			for (int i = 0; i < gamepadStates.length; i++) {
				//noinspection resource
				gamepadStates[i] = GLFWGamepadState.create();
			}
			//noinspection EmptyTryBlock,unused
			try (
					final var _ = glfwSetKeyCallback(window, (_, key, scanCode, action, mods) -> {
						// Logger.info("InputGlfw: key: key={}, scanCode={}, action={}, mods={}", key, scanCode, action, mods);
						if (action == GLFW_PRESS) {
							if ((key & 0xffff_ffffL) <= GLFW_KEY_LAST)
								keyState[key >>> 3] |= (byte)(1 << (key & 7));
							fireEvent(EventHandler.KEY_PRESS, key, 0, 0);
						} else if (action == GLFW_RELEASE) {
							if ((key & 0xffff_ffffL) <= GLFW_KEY_LAST)
								keyState[key >>> 3] &= (byte)~(1 << (key & 7));
							fireEvent(EventHandler.KEY_RELEASE, key, 0, 0);
						}
					});

//					final var _ = glfwSetCharCallback(window, (_, codePoint) -> {
//						Logger.info("InputGlfw: char: codePoint={}", codePoint);
//					});

					final var _ = glfwSetCursorPosCallback(window, (_, x, y) -> {
						// Logger.info("InputGlfw: cursor: x={}, y={}", x, y);
						int ix = (int)x, iy = (int)y;
						mouseX = ix;
						mouseY = iy;
						fireEvent(EventHandler.MOUSE_MOVE, 0, ix, iy);
					});

					final var _ = glfwSetMouseButtonCallback(window, (_, button, action, mods) -> {
						// Logger.info("InputGlfw: mouse: button={}, action={}, mods={}", button, action, mods);
						if (action == GLFW_PRESS) {
							if ((button & 0xffff_ffffL) <= GLFW_MOUSE_BUTTON_LAST)
								mouseState |= (byte)(1 << button);
							fireEvent(EventHandler.MOUSE_PRESS, button, mouseX, mouseY);
						} else if (action == GLFW_RELEASE) {
							if ((button & 0xffff_ffffL) <= GLFW_MOUSE_BUTTON_LAST)
								mouseState &= (byte)~(1 << button);
							fireEvent(EventHandler.MOUSE_RELEASE, button, mouseX, mouseY);
						}
					});

					final var _ = glfwSetScrollCallback(window, (_, x, y) -> {
						// Logger.info("InputGlfw: scroll: x={}, y={}", x, y);
						fireEvent(EventHandler.MOUSE_SCROLL, 0, (int)x, (int)y);
					})) {
			}
		} catch (Throwable e) { // rethrow
			destroy();
			throw e;
		}
	}

	@Override
	public void destroy() {
		//noinspection EmptyTryBlock
		try (final var _ = glfwSetScrollCallback(window, null);
			 final var _ = glfwSetMouseButtonCallback(window, null);
			 final var _ = glfwSetCursorPosCallback(window, null);
//			 final var _ = glfwSetCharCallback(window, null);
			 final var _ = glfwSetKeyCallback(window, null)) {
		}
		Arrays.fill(gamepadStates, null); // native memory will be freed by gc
		window = 0;
	}

	@Override
	public int getKeyboardState(int keyCode) {
		return (keyCode & 0xffff_ffffL) <= GLFW_KEY_LAST ? (keyState[keyCode >>> 3] >>> (keyCode & 7)) & 1 : 0;
	}

	@Override
	public int getMouseState(int keyCode) {
		return (keyCode & 0xffff_ffffL) <= GLFW_MOUSE_BUTTON_LAST ? (mouseState >> keyCode) & 1 : 0;
	}

	@Override
	public int getMouseX() {
		return mouseX;
	}

	@Override
	public int getMouseY() {
		return mouseY;
	}

	/**
	 * {@link org.lwjgl.glfw.GLFW#GLFW_GAMEPAD_BUTTON_LAST} {@link org.lwjgl.glfw.GLFW#GLFW_GAMEPAD_AXIS_LAST}
	 */
	@Override
	public int getGamepadStates(int index, float @Nullable [] axes) {
		if ((index & 0xffff_ffffL) >= JOYSTICK_MAX_COUNT || !gamepadConnects[index])
			return -1;
		var ptr = gamepadStates[index].address();
		if (axes != null) {
			int axesOffset = GLFWGamepadState.AXES;
			for (int i = 0, n = Math.min(axes.length, GLFW_GAMEPAD_AXIS_LAST + 1); i < n; i++)
				axes[i] = UnsafeUtil.getFloat(ptr + axesOffset + i * 4L);
		}
		return toGamepadButtonFlags(ptr);
	}

	private static int toGamepadButtonFlags(long gamepadStatePtr) {
		int s = 0;
		gamepadStatePtr += GLFWGamepadState.BUTTONS;
		for (int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++) {
			if (UnsafeUtil.getByte(gamepadStatePtr + i) == GLFW_PRESS)
				s |= 1 << i;
		}
		return s;
	}

	@Override
	public void poll() {
		for (int i = 0; i < JOYSTICK_MAX_COUNT; i++) {
			var gamepadState = gamepadStates[i];
			int oldS = toGamepadButtonFlags(gamepadState.address());
			gamepadConnects[i] = glfwGetGamepadState(GLFW_JOYSTICK_1 + i, gamepadState);
			int newS = toGamepadButtonFlags(gamepadState.address());
			int diff = oldS ^ newS;
			if (diff != 0) {
				for (int j = 0; j <= GLFW_GAMEPAD_BUTTON_LAST; j++) {
					if (((diff >> j) & 1) != 0) {
						fireEvent(((newS >> j) & 1) != 0 ? EventHandler.GAMEPAD_PRESS : EventHandler.GAMEPAD_RELEASE,
								(i << 16) + j, 0, 0);
					}
				}
			}
		}
	}

	@Override
	public void fireEvent(int event, int keyCode, int x, int y) {
		for (int i = 0, n = eventListeners.size(); i < n; i++)
			eventListeners.get(i).onEvent(event, keyCode, x, y);
		for (int i = eventHandlers.size(); --i >= 0; )
			if (eventHandlers.get(i).onEvent(event, keyCode, x, y))
				break;
	}

	@Override
	public void addEventListener(@NotNull EventHandler handler) {
		Engine.current().postEvent(() -> eventListeners.add(handler));
	}

	@Override
	public void removeEventListener(@NotNull EventHandler handler) {
		Engine.current().postEvent(() -> eventListeners.remove(handler));
	}

	@Override
	public void addEventHandler(@NotNull EventHandler handler) {
		Engine.current().postEvent(() -> eventHandlers.add(handler));
	}

	@Override
	public void removeEventHandler(@NotNull EventHandler handler) {
		Engine.current().postEvent(() -> eventHandlers.remove(handler));
	}
}
